<?php

namespace StudyBuddy\Forms;

use StudyBuddy\Request;
use StudyBuddy\Registry;
use StudyBuddy\StudyBuddyObject;

/**
 * This is base class for various web forms
 * It is responsible for rendering a form
 * using a tpl... template file, possibly
 * setting error message in the form,
 * It may also translate element titles and captions
 * as well as error messages.
 *
 */
class Form extends StudyBuddyObject {

    /**
     * Use CSRF token
     * by default it will set value of token
     * in new form and will automatically
     * validate value of submitted token
     * if form is submitted (Request was POST)
     *
     * @var bool
     */
    protected $useToken = false;

    /**
     * Array of field names used in current form
     * This should be set in sub-class that represents
     * concrete form
     *
     * This is optional and if set, then
     * keys are field names, values are... not sure yet,
     * could be objects or arrays containing validator
     * callback functions
     *
     * It is helpful to know which field names we have
     * or going to have in the form.
     *
     * First it can help if we need to pre-populate
     * field values in case of error during validation,
     * we can just get already submitted values from Request
     *
     * @var array
     */
    protected $aFields = array();

    /**
     * Array of validator callback functions
     * keys are field names, values are anonymous functions
     * that take field name as param
     *
     * @var array
     */
    protected $aValidators = array();

    /**
     * Name of form template file
     * The name of actual template should be
     * set in sub-class
     *
     * Templates must have these placeholders:
     * filedName, fieldName_e for setting
     * error specific to form field
     *
     * and also 'formError'
     * with corresponding html in template: <div class="form_error">%%</div>
     * for setting form error
     * via javascript %% should correspond to 'formError'
     * position in vars array, for example %19$s
     *
     *
     * @var string
     */
    protected $template;

    /**
     * Array of template vars
     * This is usually a copy from template
     * we get it via tplXXXX::getVars()
     * then we can work with it like
     * translating field values,
     * pre-populating fields if form has already been
     * submitted but contains errors in which case we want
     * to show user errors but also preserve already
     * submitted data in the form
     *
     * @var array
     */
    protected $aVars;

    /**
     * Flag indicates that form has been
     * submitted via POST
     *
     * @var bool
     */
    protected $bSubmitted = false;

    /**
     * Array of uploaded files
     * Basically a copy of $_FILES array that php
     * provides
     *
     * @var array
     */
    protected $aUploads = array();

    /**
     * Array of form field errors
     * keys should be form field names + '_e', values
     * are array of error messages. This way a single form field
     * can have more than one validation error message
     *
     * Before form template is parsed, this array is checked
     * and if not empty it is merged with $aVars array, then
     * merged array is used in parse() of template
     *
     * @var array
     */
    protected $aErrors = array();

    /**
     * Translation object
     *
     * @var Object of type StudyBuddy\I18n\Translator
     */
    protected $Tr;

    public function __construct(Registry $oRegistry, $useToken = false) {
        $this->oRegistry = $oRegistry;
        $this->Tr = $oRegistry->Tr;

        $this->useToken = $useToken;
        $tpl = $this->template;
        d('tpl: ' . $tpl);


        $this->aVars = $tpl::getVars();
        d('$this->aVars: ' . print_r($this->aVars, 1));
        if ('POST' === Request::getRequestMethod()) {
            $this->bSubmitted = true;
            if (true === $useToken) {
                self::validateToken($oRegistry);
            }
            $this->aUploads = $_FILES;
            d('$this->aUploads: ' . print_r($this->aUploads, 1));
        } else {
            $this->addToken();
        }

        $this->init();
    }

    /**
     * Translator method
     * It's customary in many projects to
     * use the single underscore
     * symbol for translation function.
     *
     * @param string $string string to translate
     * @param array $vars optional array of replacement vars for
     * translation
     * 
     * @return string translated string
     */
    protected function _($string, array $vars = null) {

        return $this->Tr->get($string, $vars);
    }

    /**
     * Check to see if form has been submitted
     *
     * @return bool true if form submitted, false
     * if not submitted
     */
    public function isSubmitted() {

        return $this->bSubmitted;
    }

    public function enableToken() {
        $this->useToken = true;

        return $this;
    }

    public function disableToken() {
        $this->useToken = false;

        d('$this->useToken: ' . $this->useToken);

        return $this;
    }

    public function addValidator($field, $func) {
        if (!is_callable($func)) {
            throw new \InvalidArgumentException('second param passed to addValidator must be a callable funcion. Was: ' . var_export($func, true));
        }
        $aFields = $this->getFields();
        if (!in_array($field, $aFields)) {
            throw new \StudyBuddy\DevException('Field ' . $field . ' does not exist in form. Cannot set validator for non-existent field aFields: ' . print_r($aFields, 1));
        }

        $this->aValidators[$field] = $func;
    }

    /**
     * Run custom validators
     * Validators can be added via addValidator() method
     * OR a sub class can implement a doValidate() method
     * which may contain all necessary validation methods and
     * must set errors via setError()
     *
     * @throws StudyBuddyDevException
     *
     * @return bool true if there are no validation errors,
     * false otherwise
     */
    public function validate() {

        if (!empty($this->aValidators)) {

            foreach ($this->aValidators as $field => $func) {
                if (!is_callable($func)) {
                    throw new \StudyBuddy\DevException('not callable');
                }

                $val = $this->getSubmittedValue($field);
                if (true !== $res = $func($val)) {
                    $this->setError($field, $res);
                }
            }
        }

        $this->doValidate();

        return $this->isValid();
    }

    /**
     * Method that invokes form
     * validation
     *
     * Concrete form class can implement its own
     * to do custom validation
     */
    protected function doValidate() {
        $this->validateTitle();
    }

    protected function validateTitle() {
        $t = $this->oRegistry->Request['title'];
        $min = $this->oRegistry->Ini->MIN_TITLE_CHARS;
        d('min title: ' . $min);
        if (\mb_strlen($t) < $min) {
            $this->setError('title', 'Title must contain at least ' . $min . ' letters');
        }

        return $this;
    }
    
    /**
     * Get values of submitted form fields
     * Returned values are sanitized by filter_var
     * and other custom sanitization we have in Request object
     *
     * @return array keys are form fields, values are submitted
     * values
     */
    public function getSubmittedValues() {
        $aFields = $this->getFields();
        $a = $this->oRegistry->Request->getArray();
        d('$aFields: ' . print_r($aFields, 1) . ' Request->getArray(): ' . print_r($a, 1) . ' POST: ' . print_r($_POST, 1));

        /**
         * Order of array_intersect_key is very important!
         */
        $ret = array_intersect_key($a, array_flip($aFields));
        d('submitted values: ' . print_r($ret, 1));

        return $ret;
    }

    /**
     *
     * Get value of certain form field
     * @param string $field
     * @throws \StudyBuddy\DevException if $field does not exist in form
     *
     * @return string value of submitted field
     */
    public function getSubmittedValue($field) {
        if (!$this->fieldExists($field)) {
            throw new \StudyBuddy\DevException('field ' . $field . ' does not exist');
        }

        return $this->oRegistry->Request[$field];
    }

    public function getSubmittedTeach() {
        $s = '';
        for ($i = 0; $i < 100; $i++) {
            if (isset($this->oRegistry->Request['sj' . $i])) {
                $s .= ($s == '') ? '1' : ',1';
            } else {
                $s .= ($s == '') ? '0' : ',0';
            }
        }//debug($s);
        return $s;
    }

    /**
     * Get path to uploaded file
     * The file is first copied to tmp directory
     *
     * @param string $field
     * @throws \StudyBuddy\DevException if move_uploaded_file operation
     * fails
     *
     * @return mixed null | false | string full path to new temporary location
     * of the uploaded file null if there is no uploaded file with this
     * element name of false if there was a problem with upload
     */
    public function getUploadedFile($field) {
        d('looking for uploaded file: ' . $field);
        if (!$this->fieldExists($field)) {
            throw new \StudyBuddy\DevException('field ' . $field . ' does not exist');
        }

        if (!array_key_exists($field, $this->aUploads)) {
            d('no such file in uploads: ' . $field);

            return null;
        }

        if (!is_array($this->aUploads[$field]) || (0 == $this->aUploads[$field]['size']) || empty($this->aUploads[$field]['tmp_name']) || ('none' == $this->aUploads[$field]['tmp_name'])) {
            d('file ' . $field . ' was not uploaded');

            return null;
        }

        /**
         * If upload was made but there was an error...
         * if 'error' code then
         * set element error? throw exception?
         * what to return?
         * element Error vs Form Error?
         * element for file upload input may be hidden by css style
         * like in case of avatar upload it is hidden initially
         * so it's better to set form error!
         *
         */
        if (UPLOAD_ERR_OK !== $errCode = $this->aUploads[$field]['error']) {
            e('Upload of file ' . $field . ' failed with error ' . $this->aUploads[$field]['error']);
            if (UPLOAD_ERR_FORM_SIZE === $errCode) {
                e('Uploaded file exceeds maximum allowed size');
            } elseif (UPLOAD_ERR_INI_SIZE === $errCode) {
                e('Uploaded file exceeds maximum upload size');
            }

            return false;
        }

        $temp_file = \tempnam(\sys_get_temp_dir(), 'uploaded');
        d('$temp_file: ' . $temp_file);

        if (false === \move_uploaded_file($this->aUploads[$field]['tmp_name'], $temp_file)) {
            d('no go with move_uploaded_file to ' . $temp_file . ' $this->aUploads: ' . print_r($this->aUploads, 1));
            throw new \StudyBuddy\DevException('Unable to copy uploaded file');
        }

        d('new file path: ' . $temp_file);

        return $temp_file;
    }
    public function getUploadedResourceFile($field) {
        d('looking for uploaded file: ' . $field);
        if (!$this->fieldExists($field)) {
            throw new \StudyBuddy\DevException('field ' . $field . ' does not exist');
        }

        if (!array_key_exists($field, $this->aUploads)) {
            d('no such file in uploads: ' . $field);

            return null;
        }

        if (!is_array($this->aUploads[$field]) || (0 == $this->aUploads[$field]['size']) || empty($this->aUploads[$field]['tmp_name']) || ('none' == $this->aUploads[$field]['tmp_name'])) {
            d('file ' . $field . ' was not uploaded');

            return null;
        }

        /**
         * If upload was made but there was an error...
         * if 'error' code then
         * set element error? throw exception?
         * what to return?
         * element Error vs Form Error?
         * element for file upload input may be hidden by css style
         * like in case of avatar upload it is hidden initially
         * so it's better to set form error!
         *
         */
        if (UPLOAD_ERR_OK !== $errCode = $this->aUploads[$field]['error']) {
            e('Upload of file ' . $field . ' failed with error ' . $this->aUploads[$field]['error']);
            if (UPLOAD_ERR_FORM_SIZE === $errCode) {
                e('Uploaded file exceeds maximum allowed size');
            } elseif (UPLOAD_ERR_INI_SIZE === $errCode) {
                e('Uploaded file exceeds maximum upload size');
            }

            return false;
        }

        $ext = substr($this->aUploads[$field]['name'], strrpos($this->aUploads[$field]['name'], '.'));
        $dir = STUDYBUDDY_DATA_DIR . 'resources' . DIRECTORY_SEPARATOR;
        $resource = strtotime(date('Y-m-d H:i:s')) . $ext;
        $temp_file = $dir . $resource;
        d('$temp_file: ' . $temp_file);
        d('mime type: ' . $this->aUploads[$field]['type']);

        $mime = $this->aUploads[$field]['type'];
        if(
                $mime == 'application/msword' 
                || $mime == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 
                || $mime == 'application/excel' 
                || $mime == 'application/vnd.ms-excel' 
                || $mime == 'application/x-excel' 
                || $mime == 'application/x-msexcel' 
                || $mime == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 
                || $mime == 'application/mspowerpoint' 
                || $mime == 'application/powerpoint' 
                || $mime == 'application/vnd.ms-powerpoint' 
                || $mime == 'application/x-mspowerpoint' 
                || $mime == 'application/vnd.openxmlformats-officedocument.presentationml.presentation' 
                || $mime == 'application/pdf' 
                || $mime == 'image/jpeg' 
                || $mime == 'image/png' 
                || $mime == 'text/plain'
            ) {
            // 
        } else {
            return false;
        }
        
        if (false === \move_uploaded_file($this->aUploads[$field]['tmp_name'], $temp_file)) {
            d('no go with move_uploaded_file to ' . $temp_file . ' $this->aUploads: ' . print_r($this->aUploads, 1));
            throw new \StudyBuddy\DevException('Unable to copy uploaded file');
        }

        d('new file path: ' . $temp_file);

//        return $resource;
        return array(
            'file' => $resource,
            'size' => $this->aUploads[$field]['size'],
            'type' => $this->aUploads[$field]['type']
        );
    }

    /**
     * Getter for $this->aUploads
     *
     * @return array raw array of $this->aUploads which is
     * the copy of the $_FILES Array
     */
    public function getUploadedFiles() {
        return $this->aUploads;
    }

    /**
     * Check if form had any uploaded files
     *
     * @return bool true if there are any uploaded files
     */
    public function hasUploads() {
        return (count($this->aUploads) > 0);
    }

    /**
     *
     * Check if certain form field exists in form object
     * @param string $field
     * @return bool
     */
    protected function fieldExists($field) {
        $aFields = $this->getFields();

        return in_array($field, $aFields);
    }

    /**
     *
     * Enter description here ...
     */
    public function getFields() {
        $aFields = (!empty($this->aFields)) ? array_keys($this->aFields) : array_keys($this->aVars);

        return $aFields;
    }

    /**
     * Sub-class may implement init() method
     * in order to initialize form vars.
     * For example to translate some of the vars into
     * current language
     *
     * @return object $this
     */
    protected function init() {

        return $this;
    }

    /**
     * Sets error message
     * for the form field
     *
     * We don't check to see if field name exists
     * and we don't check if field_e key exists
     * in template vars. If it does not then
     * setting of error will not fail but will have
     * absolutely no meaning since errors will not be shown
     * on form.
     *
     * @todo in the future will automatically
     * translate the $message into current language
     * using oI18n
     *
     *
     * @param string $field
     * @param string $message
     */
    public function setError($field, $message) {
        if (Request::isAjax()) {
            \StudyBuddy\Responder::sendJSON(array('formElementError' => array($field => $message)));
        } else {
            $this->aErrors[$field . '_e'][] = $message;
        }

        return $this;
    }

    /**
     * Set error message for the form as a whole.
     * This error message is not specific to any form field,
     * it usually appears on top of form as a general error message
     *
     * For example: You must wait 5 minutes between posting
     * This is not due to any element error, just a general error
     * message.
     *
     * The form template MUST have 'formError' variable in it!
     *
     * @param string $errMessage
     */
    public function setFormError($errMessage) {

        if (Request::isAjax()) {
            \StudyBuddy\Responder::sendJSON(array('formError' => $errMessage));
        } else {
            $this->aErrors['formError'][] = $errMessage;
        }

        return $this;
    }

    /**
     *
     * Set variable (any variable that
     * is present in form's template
     *
     * @param string $name
     * @param string $value
     * @throws \InvalidArgumentException
     *
     * @return object $this
     */
    public function setVar($name, $value) {
        if (!array_key_exists($name, $this->aVars)) {

            throw new \InvalidArgumentException('Var ' . $name . ' does not exist in this form\'s template aVars: ' . print_r($this->aVars, 1));
        }

        $this->aVars[$name] = $value;

        return $this;
    }

    /**
     *
     * Magic setter
     * @param string $name
     * @param string $val
     */
    public function __set($name, $val) {
        $this->setVar($name, $val);
    }

    /**
     * Getter for $this->aErrors
     * @return array
     */
    public function getErrors() {

        return $this->aErrors;
    }

    /**
     * Parse form template using vars/values we set
     * also if aErrors not empty, merge it with aVars
     *
     * @param bool $useSubmittedVars if set to false then
     * will not update $this->aVars to the values of submitted
     * values and will reuse the vars that were set initially.
     * This is useful when form was submitted but then some error
     * occured in a script that was parsing the form.
     * In that case
     * we often need to setFormError and then use values in form
     * than were there initially, no using any of the submitted values.
     *
     * @return string html parsed form template
     */
    public function getForm($useSubmittedVars = true) {
        d('$this->aVars: ' . print_r($this->aVars, 1));

        if ($useSubmittedVars) {
            $this->prepareVars();
        }

        $this->addErrors();
        $tpl = $this->template;

        /**
         * Observer can
         * do setVar() on a passed form object
         * and add another element to aVars just before
         * form is rendered
         *
         */
        $this->oRegistry->Dispatcher->post($this, 'onBeforeFormRender');

        return $tpl::parse($this->aVars);
    }

    /**
     * @todo here we can do translation of template vars
     * We will have translateArray() in oTr which will
     * take array as input, then use keys as strings and values
     * as fallback values and return array with translated values
     *
     * @return object $this
     */
    protected function prepareVars() {

        if ($this->bSubmitted) {
            $a = $this->oRegistry->Request->getArray();
            d('a from request: ' . print_r($a, 1));
            d('$this->aVars : ' . print_r($this->aVars, 1));

//            $this->aVars = array_merge($this->aVars, $a);
            $this->aVars = array_merge($a, $this->aVars);
        }

        return $this;
    }

    /**
     * It makes sense to call this method ONLY after
     * you validated the form values yourself and
     * set errors via setError() method
     *
     * @return bool true if no errors has been set,
     * false otherwise
     *
     */
    public function isValid() {

        return 0 === count($this->aErrors);
    }

    /**
     * If aErrors not empty then merge aVars with aErrors
     *
     * @return object $this
     */
    protected function addErrors() {
        if (!empty($this->aErrors)) {
            $this->aVars = array_merge($this->aVars, $this->flattenErrors());
            d('$this->aVars: ' . print_r($this->aVars, 1));
        }

        return $this;
    }

    /**
     * Turn array of errors into string
     * in which each element from array becomes
     * an <li> html element
     *
     * @return array where keys are field names + _e
     * and values are strings contained in <ul> tag
     *
     */
    protected function flattenErrors() {
        $ret = array();
        foreach ($this->aErrors as $field => $aErrors) {
            $ret[$field] = '<ul>';
            foreach ($aErrors as $error) {
                $ret[$field] .= '<li>' . $error . '</li>';
            }
            $ret[$field] .= '</ul>';
        }
        d('$ret: ' . print_r($ret, 1));

        return $ret;
    }

    /**
     * Generate unique ID and store in session
     * The page will have the meta tag 'version'
     * with the value of this token
     * it will be used by ajax based forms when submitting
     * form via ajax
     *
     *
     * @return string value of form token
     * for this class.
     */
    public static function generateToken() {
        if (!array_key_exists('secret', $_SESSION)) {

            $token = uniqid(mt_rand());
            //$_SESSION['secret'] = $token;
            $_SESSION['secret'] = hash('md5', $token);
        }

        return $_SESSION['secret'];
        //return hash('md5', $_SESSION['secret'].get_called_class());
    }

    /**
     * Add value of 'token' to form's aVars
     *
     * @return object $this
     */
    protected function addToken() {
        if ($this->useToken) {
            $this->aVars['token'] = static::generateToken();
        }

        return $this;
    }

    /**
     * Validate submitted 'token' value
     * agains generateToken()
     * they must match OR throw TokenException
     *
     * Must be static because we use this sometimes
     * from outside this object.
     *
     * @return true on success
     *
     */
    public static function validateToken(Registry $oRegistry) {

        if (empty($_SESSION['secret'])) {
            throw new \StudyBuddy\TokenException('Form token not found in session');
        }

        $token = $oRegistry->Request['token'];
        d('submitted form token: ' . $token);
        if ($token !== $_SESSION['secret']) {
            throw new \StudyBuddy\TokenException('Invalid security token. You need to reload this page in browser and try submitting this form again');
        }

        return true;
    }

    /**
     * Translate some vars like labels and
     * descriptions of form elements
     *
     * @todo actually translate those
     *
     */
    protected function translateVars() {
        d('cp');

        return $this;
    }

}
